分散アプリケーションランタイム「Dapr」で構築したアプリケーションをEKS上にデプロイしてみる
CX事業本部@大阪の岩田です。Microsoftがリードする分散アプリケーションランタイムDaprについて調べる機会があったので、試しにEKS上にサンプルアプリをデプロイして動作させるまでの流れを試してみました。せっかくなので調べた内容やデプロイ手順について紹介させていただきます。
Daprとは
DaprはOSSでMicrosoftがリードするOSSの分散アプリケーション向けランタイムです。Daprによって分散アプリケーションを構築するために必要な基本機能が提供されるため、開発者はこれらの機能を自作する必要がなくなります。
以下はGitHubのDaprリポジトリで紹介されているDaprの概要です。
様々なプログラミング言語からHTTP APIもしくはgRPC APIを通じてビルディングブロックと呼ばれる様々な機能が利用できることが分かります。Daprは任意の環境で実行できるように設計されており、MicrosoftのクラウドサービスであるAzure以外にもAWSやGCPといったクラウド環境上での動作がサポートされています。
サポートされる言語
様々な言語向けにDaprのSDKが提供されており、SDKを利用することで簡単にDaprをアプリケーションに組み込めます。SDKは
- Daprのビルディングブロックを呼び出すためのClient SDK
- 別サービスからの呼び出しやトピックのサブスクライブを実現するための利用するServer extensions
- 仮想アクターを構築するためのActor SDK
の3種に分かれます。
2021/7時点での言語別各種SDKの対応状況は以下の通りです。
Language | Status | Client SDK | Server extensions | Actor SDK |
---|---|---|---|---|
.NET | Stable | ✔ | ASP.NET Core | ✔ |
Python | Stable | ✔ | gRPC | FastAPI Flask |
Java | Stable | ✔ | Spring Boot | ✔ |
Go | Stable | ✔ | ✔ | |
PHP | Stable | ✔ | ✔ | ✔ |
C++ | In development | ✔ | ||
Rust | In development | ✔ | ||
Javascript | In development | ✔ |
ビルディングブロック
Daprはいくつかのビルディングブロックから構成されており、開発者は自分に必要なビルディングブロックだけを選択して利用できように構成されています。提供されるビルディングブロックは以下の通りで、分散アプリケーションの構築に必要となる一通りの機能を有していることがわかります
Service invocation
gRPC APIもしくはHTTP APIを利用して、別のアプリケーションをセキュアに呼び出すためのビルディングブロックです
- 名前空間
- mTLSによる認証
- アクセスコントロール
- リトライ
- サービスディスカバリ
- ロードバランシング
- トレーシング
といった機能が提供されます
State management
キー/バリュー形式でステート管理を実現するためのビルディングブロックです
ステートの保存先として
- MongoDB
- Redis
- Azure Cosmos DB
- Azure Blob Storage
等のコンポーネントがサポートされています
Publish & subscribe
いわゆるPub/Subですね。メッセージのPublishとSubscribeを実現するためのビルディングブロックです。
- メッセージのルーティング
- At-least-onceなメッセージ配信の保証
- コンシューマーグループの管理
といった機能が提供されます
Bindings
外部システムとDaprを接続するためのビルディングブロックです。外部システムのポーリングやリトライ処理といった定型的な処理はBindingsに任せることで、自前実装が不要になります。
Bindingsは外部リソースのイベントをトリガーにアプリケーションをトリガーするためのInput bindings、外部リソースを呼び出すためのOutput bindings2つの機能が提供されます。Bindingsを利用すると、アプリケーションコードからはデータの保存先がS3なのか?それともDynamoDBなのか?といったことを意識せずにデータを保存できるようになります。
GitHubのリポジトリでは様々な外部リソース向けのBindingsが開発されています
https://github.com/dapr/components-contrib/tree/master/bindings
Actors
任意の言語、プラットフォームでアクターモデルを利用するためのビルディングブロックです。アクターパターンはメッセージを受信して1度に1つずつ処理する自己完結型のユニット=アクターとしてコードを記述します。
アクターモデルはシングル スレッドで動作し、複数のアクターが同時実行可能ですが、各アクターは受信したメッセージを一度に 1 つずつ処理します。 アクター内でアクティブなスレッドは常に1つ以下ということが保証され、同時実行システムや並列システムを簡単に作成できるようになります。
Observability
可観測性を提供するビルディングブロックです。
- 分散トレーシング
- メトリックの収取
- ログの収集
- ヘルスチェック
といった機能をサポートします。このビルディングブロックはOpenTelemetryとZipkinをサポートするため、New RelicやDataDogといった外部サービスとの連携も容易に実現できます。
Secrets management
DBの接続文字列、APIキー、クライアント証明書といったシークレットを管理するためのビルディングブロックです。このビルディングブロックを利用すると、アプリケーションコードから
- 環境変数
- ローカル ファイル
- Kubernetes シークレット
- AWS Secrets Manager
- Azure Key Vault
- GCP Secret Manager
- HashiCorp Vault
といったシークレットストアを簡単に利用できるようになります
やってみる
なんとなくDaprの概要が分かったので、実際にサンプルアプリを動かしてみます。今回は以下のリポジトリで提供されているサンプルアプリHello KubernetesをEKS上にデプロイしてみます
https://github.com/dapr/quickstarts/tree/master/hello-kubernetes
環境
今回利用した環境です
- Kubernetes: 1.19
- eksctl: 0.56.0
- kubectl: v1.19.6-eks-49a6c0
- Dapr CLI: 1.2.0
- Helm: v3.6.2+gee407bd
今回はCloudShell環境に諸々のCLIツールをインストールし、CloudShell上からコマンドを実行していきます。
EKSクラスタの構築
まずEKSクラスタを構築します
$ eksctl create cluster --name dapr-cluster --managed
CloudFormationのスタックがデプロイされるので
- ksctl-dapr-cluster-cluster
- eksctl-dapr-cluster-nodegroup-ng-xxxxxxxx
2つのスタックがCREATE_COMPLETE状態に変わるまで待ちます。スタックのデプロイが完了したら簡単に動作確認しておきましょう。
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 19m
$ kubectl get node NAME STATUS ROLES AGE VERSION ip-192-168-25-77.ap-northeast-1.compute.internal Ready <none> 9m44s v1.19.6-eks-49a6c0 ip-192-168-62-49.ap-northeast-1.compute.internal Ready <none> 9m43s v1.19.6-eks-49a6c0
Daprの初期化
続いてEKSクラスタにDaprのコンポーネントをデプロイします
$ dapr init --kubernetes ⌛ Making the jump to hyperspace... ℹ️ Note: To install Dapr using Helm, see here: https://docs.dapr.io/getting-started/install-dapr-kubernetes/#install-with-helm-advanced ✅ Deploying the Dapr control plane to your cluster... ✅ Success! Dapr has been installed to namespace dapr-system. To verify, run `dapr status -k' in your terminal. To get started, go here: https://aka.ms/dapr-getting-started
デプロイされたサービスを確認してみます
$ kubectl get svc --namespace dapr-system NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE dapr-api ClusterIP 10.100.180.138 <none> 80/TCP 9m37s dapr-dashboard ClusterIP 10.100.56.127 <none> 8080/TCP 9m37s dapr-placement-server ClusterIP None <none> 50005/TCP,8201/TCP 9m37s dapr-sentry ClusterIP 10.100.29.44 <none> 80/TCP 9m37s dapr-sidecar-injector ClusterIP 10.100.178.19 <none> 443/TCP 9m37s
デプロイされたPodは以下の通りです
$ kubectl get pods --namespace dapr-system NAME READY STATUS RESTARTS AGE dapr-dashboard-58b4647996-4fzs2 1/1 Running 0 10m dapr-operator-85bdd7d89d-9tndp 1/1 Running 0 10m dapr-placement-server-0 1/1 Running 0 10m dapr-sentry-76bfc5f7c7-wqhs6 1/1 Running 0 10m dapr-sidecar-injector-786645f444-ph62k 1/1 Running 0 10m
RedisのState Storeをデプロイ
今回デプロイするサンプルアプリHello KubernetesはState StoreにRedisを利用します。Helmを使ってRedisのState Storeを作成しておきましょう。
まずはリポジトリの追加
$ helm repo add bitnami https://charts.bitnami.com/bitnami "bitnami" has been added to your repositories $ helm repo update Hang tight while we grab the latest from your chart repositories... ...Successfully got an update from the "bitnami" chart repository Update Complete. ⎈Happy Helming!⎈
続いてbitnami/redis
チャートをインストール
$ helm install redis bitnami/redis NAME: redis LAST DEPLOYED: Sun Jul 18 04:16:20 2021 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: ** Please be patient while the chart is being deployed ** Redis(TM) can be accessed on the following DNS names from within your cluster: redis-master.default.svc.cluster.local for read/write operations (port 6379) redis-replicas.default.svc.cluster.local for read-only operations (port 6379) To get your password run: export REDIS_PASSWORD=$(kubectl get secret --namespace default redis -o jsonpath="{.data.redis-password}" | base64 --decode) To connect to your Redis(TM) server: 1. Run a Redis(TM) pod that you can use as a client: kubectl run --namespace default redis-client --restart='Never' --env REDIS_PASSWORD=$REDIS_PASSWORD --image docker.io/bitnami/redis:6.2.4-debian-10-r13 --command -- sleep infinity Use the following command to attach to the pod: kubectl exec --tty -i redis-client \ --namespace default -- bash 2. Connect using the Redis(TM) CLI: redis-cli -h redis-master -a $REDIS_PASSWORD redis-cli -h redis-replicas -a $REDIS_PASSWORD To connect to your database from outside the cluster execute the following commands: kubectl port-forward --namespace default svc/redis-master 6379:6379 & redis-cli -h 127.0.0.1 -p 6379 -a $REDIS_PASSWORD
Redisのサービス、Podが動作していることを確認しておきます
$ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 37m redis-headless ClusterIP None <none> 6379/TCP 3m6s redis-master ClusterIP 10.100.237.250 <none> 6379/TCP 3m6s redis-replicas ClusterIP 10.100.16.88 <none> 6379/TCP 3m6s
$ kubectl get pods NAME READY STATUS RESTARTS AGE redis-master-0 1/1 Running 0 3m17s redis-replicas-0 1/1 Running 0 3m17s redis-replicas-1 1/1 Running 0 2m27s redis-replicas-2 1/1 Running 0 110s
サンプルアプリのデプロイ
準備ができたので、サンプルアプリをデプロイしていきます。まずGitHubのリポジトリをクローン
$ git clone https://github.com/dapr/quickstarts.git Cloning into 'quickstarts'... remote: Enumerating objects: 2593, done. remote: Counting objects: 100% (295/295), done. remote: Compressing objects: 100% (151/151), done. remote: Total 2593 (delta 164), reused 233 (delta 134), pack-reused 2298 Receiving objects: 100% (2593/2593), 10.30 MiB | 26.04 MiB/s, done. Resolving deltas: 100% (1528/1528), done.
Node.jsのサンプルアプリをデプロイします
$ cd quickstarts/hello-kubernetes $ kubectl apply -f ./deploy/node.yaml service/nodeapp created deployment.apps/nodeapp created
サンプルアプリのソースコードはこちらです。State StoreとしてはRedisを利用していますが、アプリケーションコードからはRedisの存在が隠蔽されており、単にHTTPリクエストを発行するだけでRedisに対して読み書きできることが分かります。
// ------------------------------------------------------------ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // ------------------------------------------------------------ const express = require('express'); const bodyParser = require('body-parser'); require('isomorphic-fetch'); const app = express(); app.use(bodyParser.json()); // These ports are injected automatically into the container. const daprPort = process.env.DAPR_HTTP_PORT; const daprGRPCPort = process.env.DAPR_GRPC_PORT; const stateStoreName = `statestore`; const stateUrl = `http://localhost:${daprPort}/v1.0/state/${stateStoreName}`; const port = 3000; app.get('/order', (_req, res) => { fetch(`${stateUrl}/order`) .then((response) => { if (!response.ok) { throw "Could not get state."; } return response.text(); }).then((orders) => { res.send(orders); }).catch((error) => { console.log(error); res.status(500).send({message: error}); }); }); app.post('/neworder', (req, res) => { const data = req.body.data; const orderId = data.orderId; console.log("Got a new order! Order ID: " + orderId); const state = [{ key: "order", value: data }]; fetch(stateUrl, { method: "POST", body: JSON.stringify(state), headers: { "Content-Type": "application/json" } }).then((response) => { if (!response.ok) { throw "Failed to persist state."; } console.log("Successfully persisted state."); res.status(200).send(); }).catch((error) => { console.log(error); res.status(500).send({message: error}); }); }); app.get('/ports', (_req, res) => { console.log("DAPR_HTTP_PORT: " + daprPort); console.log("DAPR_GRPC_PORT: " + daprGRPCPort); res.status(200).send({DAPR_HTTP_PORT: daprPort, DAPR_GRPC_PORT: daprGRPCPort }) }); app.listen(port, () => console.log(`Node App listening on port ${port}!`));
3つのルートが定義されたExpressのアプリで、それぞれの処理概要は以下の通りです
- GET /ports
- Daprのポート番号を返却する
- POST /neworder
- State Storeに注文データを保存する
- GET /order
- State Storeから注文データを取得する
デプロイ完了後に確認すると、nodeappという名前のサービスが動作していることが分かります。
$ kubectl get svc nodeapp NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nodeapp LoadBalancer 10.100.241.126 xxxxxx.ap-northeast-1.elb.amazonaws.com 80:31990/TCP 96s
Podの前段にELBがデプロイされているので、ELBのFQDNを取得します
$ export NODE_APP=$(kubectl get svc nodeapp --output 'jsonpath={.status.loadBalancer.ingress[0].hostname}')
curlコマンドで簡単に動作確認します
$ curl $NODE_APP/ports {"DAPR_HTTP_PORT":"3500","DAPR_GRPC_PORT":"50001"}
レスポンスが返却されました。ちゃんと動いてそうですね。
続いてPythonのサンプルアプリをデプロイします。こちらのサンプルアプリはState Storeに注文データを送信し続けるアプリです。
# ------------------------------------------------------------ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------------------------------ import os import requests import time dapr_port = os.getenv("DAPR_HTTP_PORT", 3500) dapr_url = "http://localhost:{}/v1.0/invoke/nodeapp/method/neworder".format(dapr_port) n = 0 while True: n += 1 message = {"data": {"orderId": n}} try: response = requests.post(dapr_url, json=message, timeout=5) if not response.ok: print("HTTP %d => %s" % (response.status_code, response.content.decode("utf-8")), flush=True) except Exception as e: print(e, flush=True) time.sleep(1)
デプロイします
$ kubectl apply -f ./deploy/python.yaml deployment.apps/pythonapp created
しばらくするとサンプルアプリのPodが起動してきます
$ kubectl get pods --selector=app=python NAME READY STATUS RESTARTS AGE pythonapp-fcb4f49b-gv4tr 2/2 Running 0 26s
Node.jsアプリのログを確認するとPythonアプリから連続して注文データがPOSTされていることが分かります
$ kubectl logs --selector=app=node -c node --tail=-1 Got a new order! Order ID: 82 Successfully persisted state. Got a new order! Order ID: 83 Successfully persisted state. ...略
Node.jsアプリのエンドポイント/order
にアクセスするとState Storeに保存された注文データがPythonアプリから上書きされていることが分かります
$ curl $NODE_APP/order {"orderId":177} $ curl $NODE_APP/order {"orderId":178}
最後に redis-cliからState Store(Redis)の中身を直接確認してみましょう
$ export REDIS_PASSWORD=$(kubectl get secret --namespace default redis -o jsonpath="{.data.redis-password}" | base64 --decode) $ kubectl exec -it redis-master-0 -- redis-cli -a $REDIS_PASSWORD Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. 127.0.0.1:6379> hgetall "nodeapp||order" 1) "data" 2) "{\"orderId\":707}" 3) "version" 4) "700"
まとめ
Daprの概要調査とサンプルアプリのデプロイまで試してみました。今回はDaprのビルディングブロックのうち、State managementぐらいしか試せていませんが、他のビルディングブロックについても改めて試してみたいと思います。